import { mat4, vec3 } from "wgpu-matrix";
import { GUI } from "dat.gui";
import {
  cubeVertexArray,
  cubeVertexSize,
  cubeUVOffset,
  cubePositionOffset,
  cubeVertexCount,
} from "../../meshes/cube";
import cubeWGSL from "./cube.wgsl";
import { ArcballCamera, WASDCamera } from "./Camera";
import { createInputHandler } from "./Input";
import { quitIfWebGPUNotAvailable } from "../../utils";
import type { Base } from "../Base";

export default class CameraRender implements Base {
  name = "Cameras";
  description?: string | undefined = "This example provides example camera implementations";
  gui?: GUI;
  // The camera types
  cameras: {
    arcball: ArcballCamera;
    WASD: WASDCamera;
  };
  oldCameraType = "arcball" as keyof typeof this.cameras;
  constructor() {
    const initialCameraPosition = vec3.create(3, 2, 5);
    this.cameras = {
      arcball: new ArcballCamera({ position: initialCameraPosition }),
      WASD: new WASDCamera({ position: initialCameraPosition }),
    };
  }

  onUpdate(data: any): void {
    const newCameraType = data.type as string as keyof typeof this.cameras;
    this.cameras[newCameraType].matrix =
      this.cameras[this.oldCameraType].matrix;
    this.oldCameraType = newCameraType;
  }
  async run(canvas: HTMLCanvasElement) {
    // The input handler
    const inputHandler = createInputHandler(window, canvas);
    const self = this;
    const adapter = await navigator.gpu?.requestAdapter({
      featureLevel: "compatibility",
    });
    const device = await adapter?.requestDevice();
    if (!adapter || !device) {
      return;
    }
    quitIfWebGPUNotAvailable(adapter, device);
    const context = canvas.getContext("webgpu") as GPUCanvasContext;

    const devicePixelRatio = window.devicePixelRatio;
    canvas.width = canvas.clientWidth * devicePixelRatio;
    canvas.height = canvas.clientHeight * devicePixelRatio;
    const presentationFormat = navigator.gpu.getPreferredCanvasFormat();

    context.configure({
      device,
      format: presentationFormat,
    });

    // Create a vertex buffer from the cube data.
    const verticesBuffer = device.createBuffer({
      size: cubeVertexArray.byteLength,
      usage: GPUBufferUsage.VERTEX,
      mappedAtCreation: true,
    });
    new Float32Array(verticesBuffer.getMappedRange()).set(cubeVertexArray);
    verticesBuffer.unmap();

    const pipeline = device.createRenderPipeline({
      layout: "auto",
      vertex: {
        module: device.createShaderModule({
          code: cubeWGSL,
        }),
        buffers: [
          {
            arrayStride: cubeVertexSize,
            attributes: [
              {
                // position
                shaderLocation: 0,
                offset: cubePositionOffset,
                format: "float32x4",
              },
              {
                // uv
                shaderLocation: 1,
                offset: cubeUVOffset,
                format: "float32x2",
              },
            ],
          },
        ],
      },
      fragment: {
        module: device.createShaderModule({
          code: cubeWGSL,
        }),
        targets: [
          {
            format: presentationFormat,
          },
        ],
      },
      primitive: {
        topology: "triangle-list",
        cullMode: "back",
      },
      depthStencil: {
        depthWriteEnabled: true,
        depthCompare: "less",
        format: "depth24plus",
      },
    });

    const depthTexture = device.createTexture({
      size: [canvas.width, canvas.height],
      format: "depth24plus",
      usage: GPUTextureUsage.RENDER_ATTACHMENT,
    });

    const uniformBufferSize = 4 * 16; // 4x4 matrix
    const uniformBuffer = device.createBuffer({
      size: uniformBufferSize,
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });

    // Fetch the image and upload it into a GPUTexture.
    let cubeTexture: GPUTexture;
    {
      const response = await fetch("../src/assets/webgpu.png");
      const imageBitmap = await createImageBitmap(await response.blob());

      cubeTexture = device.createTexture({
        size: [imageBitmap.width, imageBitmap.height, 1],
        format: "rgba8unorm",
        usage:
          GPUTextureUsage.TEXTURE_BINDING |
          GPUTextureUsage.COPY_DST |
          GPUTextureUsage.RENDER_ATTACHMENT,
      });
      device.queue.copyExternalImageToTexture(
        { source: imageBitmap },
        { texture: cubeTexture },
        [imageBitmap.width, imageBitmap.height]
      );
    }

    // Create a sampler with linear filtering for smooth interpolation.
    const sampler = device.createSampler({
      magFilter: "linear",
      minFilter: "linear",
    });

    const uniformBindGroup = device.createBindGroup({
      layout: pipeline.getBindGroupLayout(0),
      entries: [
        {
          binding: 0,
          resource: {
            buffer: uniformBuffer,
          },
        },
        {
          binding: 1,
          resource: sampler,
        },
        {
          binding: 2,
          resource: cubeTexture.createView(),
        },
      ],
    });

    const renderPassDescriptor: GPURenderPassDescriptor = {
      colorAttachments: [
        {
          view: undefined as unknown as GPUTextureView, // Assigned later
          clearValue: [0.5, 0.5, 0.5, 1.0],
          loadOp: "clear",
          storeOp: "store",
        },
      ],
      depthStencilAttachment: {
        view: depthTexture.createView(),

        depthClearValue: 1.0,
        depthLoadOp: "clear",
        depthStoreOp: "store",
      },
    };

    const aspect = canvas.width / canvas.height;
    const projectionMatrix = mat4.perspective(
      (2 * Math.PI) / 5,
      aspect,
      1,
      100.0
    );
    const modelViewProjectionMatrix = mat4.create();

    function getModelViewProjectionMatrix(deltaTime: number) {
      const camera = self.cameras[self.oldCameraType];
      const viewMatrix = camera.update(deltaTime, inputHandler());
      mat4.multiply(projectionMatrix, viewMatrix, modelViewProjectionMatrix);
      return modelViewProjectionMatrix;
    }

    let lastFrameMS = Date.now();

    function frame() {
      const now = Date.now();
      const deltaTime = (now - lastFrameMS) / 1000;
      lastFrameMS = now;

      const modelViewProjection = getModelViewProjectionMatrix(deltaTime);
      device?.queue.writeBuffer(
        uniformBuffer,
        0,
        modelViewProjection.buffer,
        modelViewProjection.byteOffset,
        modelViewProjection.byteLength
      );
      // @ts-ignore
      renderPassDescriptor.colorAttachments[0].view = context
        .getCurrentTexture()
        .createView();

      const commandEncoder = device?.createCommandEncoder();
      const passEncoder = commandEncoder?.beginRenderPass(renderPassDescriptor);
      passEncoder?.setPipeline(pipeline);
      passEncoder?.setBindGroup(0, uniformBindGroup);
      passEncoder?.setVertexBuffer(0, verticesBuffer);
      passEncoder?.draw(cubeVertexCount);
      passEncoder?.end();
      // @ts-ignore
      device?.queue.submit([commandEncoder.finish()]);

      requestAnimationFrame(frame);
    }
    requestAnimationFrame(frame);
  }

  destroy(): void {
    if (this.gui?.name === "Camera") {
      this.gui.destroy();
    }
  }
}
